import {Directive, DirectiveBinding} from "vue";

const ChangePositionType = {
  Start: 1,
  End: 2,
  Move: 3,
};
(function (ChangePositionType) {
  ChangePositionType[ChangePositionType['Start'] = 1] = 'Start'
  ChangePositionType[ChangePositionType['End'] = 2] = 'End'
  ChangePositionType[ChangePositionType['Move'] = 3] = 'Move'
})(ChangePositionType)


export const draggable: Directive = {
  mounted: function (el, binding) {
    new DragHandler(el, binding);
  },
  unmounted(el) {
    el.removeEventListener('mousedown', el['listener'])
  }
}

class DragHandler {
  private readonly dragNode: HTMLElement;
  private handler: HTMLElement;
  private readonly binding: DirectiveBinding;
  private readonly el: HTMLElement;
  private readonly mouseDownListener: any;
  private readonly mouseMoveListener: any;
  private readonly mouseUpListener: any;
  private state: any = {};
  private boundingRect: any = {};
  private highlightColor?: string;
  private titleColor?: string;
  private zIndex?: string;

  constructor(el: HTMLElement, binding: DirectiveBinding) {
    let bindingValue = binding.value;
    if (bindingValue && bindingValue.stopDragging) {
      return
    }
    if (!bindingValue) {
      bindingValue = binding.value = {};
    }
    if (bindingValue && bindingValue.dragNodeId) {
      this.dragNode = document.getElementById(bindingValue.dragNodeId);
    }
    this.el = el;
    this.handler = this.dragNode || el;
    this.handler.style.cursor = 'move';
    this.binding = binding;
    this.highlightColor = binding.value.highlightColor;
    this.titleColor = this.handler.style.background;
    this.zIndex = this.handler.style.zIndex;
    if (bindingValue.boundingNodeId) {
      bindingValue.boundingElement = document.getElementById(bindingValue.boundingNodeId);
    }
    if (!bindingValue.boundingElement && bindingValue.boundingWithBody) {
      binding.value.boundingElement = document.body
    }
    if (binding && bindingValue && bindingValue.resetInitialPos) {
      this.initializeState()
      this.handlePositionChanged()
    }
    this.boundingRect = this.getBoundingRect();
    if (!this.handler.getAttribute('draggable')) {
      el.removeEventListener('mousedown', el['listener'])
      this.mouseDownListener = this.mouseDown.bind(this);
      this.mouseMoveListener = this.mouseMove.bind(this);
      this.mouseUpListener = this.mouseUp.bind(this);
      this.handler.addEventListener('mousedown', this.mouseDownListener);
      // let listener = ()=>{
      //   this.boundingRect = this.getBoundingRect();
      // };
      // window.addEventListener('resize',listener)
      this.handler.setAttribute('draggable', 'true')
      el['listener'] = this.mouseDownListener;
      this.initializeState();
      this.handlePositionChanged();
    }
  }

  mouseMove(event:Event) {
    event.preventDefault()
    let stopDragging = this.binding.value && this.binding.value.stopDragging
    if (stopDragging) {
      return
    }
    let state = this.state
    if (!state.startDragPosition || !state.initialMousePos) {
      this.initializeState(event)
      state = this.state
    }
    let dx = event.clientX - state.initialMousePos.left
    let dy = event.clientY - state.initialMousePos.top
    let currentDragPosition = {
      left: state.startDragPosition.left + dx,
      top: state.startDragPosition.top + dy
    }
    let boundingRect = this.boundingRect;
    let elementRect = this.el.getBoundingClientRect()
    if (boundingRect && elementRect) {
      currentDragPosition = this.getPosWithBoundaries(elementRect, boundingRect, currentDragPosition.left, currentDragPosition.top, this.binding.value.boundingRectMargin)
    }
    this.state = Object.assign(this.state, {currentDragPosition: currentDragPosition})
    this.updateElementStyle()
    this.handlePositionChanged(event)
  }

  getPosWithBoundaries(elementRect, boundingRect, left, top, boundingRectMargin) {
    if (boundingRectMargin === void 0) {
      boundingRectMargin = {}
    }
    let adjustedPos = {left: left, top: top}
    let height = elementRect.height, width = elementRect.width
    let topRect = top, bottomRect = top + height, leftRect = left, rightRect = left + width
    let marginTop = boundingRectMargin.top || 0, marginBottom = boundingRectMargin.bottom || 0,
      marginLeft = boundingRectMargin.left || 0, marginRight = boundingRectMargin.right || 0
    let topBoundary = boundingRect.top + marginTop, bottomBoundary = boundingRect.bottom - marginBottom,
      leftBoundary = boundingRect.left + marginLeft, rightBoundary = boundingRect.right - marginRight
    if (topRect < topBoundary) {
      adjustedPos.top = topBoundary
    } else if (bottomRect > bottomBoundary) {
      adjustedPos.top = bottomBoundary - height
    }
    if (leftRect < leftBoundary) {
      adjustedPos.left = leftBoundary
    } else if (rightRect > rightBoundary) {
      adjustedPos.left = rightBoundary - width
    }
    return adjustedPos
  }

  getBoundingRect() {
    if (!this.binding.value) {
      return
    }
    return this.binding.value.boundingRect
      || this.binding.value.boundingElement
      && this.binding.value.boundingElement.getBoundingClientRect()
  }

  updateElementStyle() {
    let state = this.state
    if (!state.currentDragPosition) {
      return
    }
    // let dragNode = el.dragNode || el
    let dragNode = this.el
    dragNode.style.position = 'absolute'
    let parentElement = this.el.parentElement;
    dragNode.style.left = (state.currentDragPosition.left-parentElement.offsetLeft) + 'px'
    dragNode.style.top = (state.currentDragPosition.top-parentElement.offsetTop) + 'px'
    // dragNode.style.zIndex = 999
  }

  mouseUp(event) {
    event.preventDefault()
    if (this.highlightColor) {
      this.handler.style.background = this.titleColor;
    }
    if (this.binding.value.highlightClass) {
      this.handler.classList.remove(this.binding.value.highlightClass);
    }
    this.el.style.zIndex = this.zIndex||'1'
    let currentRectPosition = this.getRectPosition()
    this.state = Object.assign(this.state, {
      initialMousePos: undefined,
      startDragPosition: currentRectPosition,
      currentDragPosition: currentRectPosition
    })
    document.removeEventListener('mousemove', this.mouseMoveListener)
    document.removeEventListener('mouseup', this.mouseUpListener)
    this.handlePositionChanged(event, ChangePositionType.End)
  }

  mouseDown(event) {
    if (this.highlightColor) {
      this.handler.style.background = this.highlightColor;
    }
    if (this.binding.value.highlightClass) {
      this.handler.classList.add(this.binding.value.highlightClass)
    }
    this.state = Object.assign(this.state, {initialMousePos: this.getInitialMousePosition(event)})
    this.handlePositionChanged(event, ChangePositionType.Start)
    document.addEventListener('mousemove', this.mouseMoveListener)
    document.addEventListener('mouseup', this.mouseUpListener)
    this.el.style.zIndex = this.binding.value.zIndex || 999
  }

  getInitialMousePosition(event) {
    return event && {
      left: event.clientX,
      top: event.clientY
    }
  }

  getRectPosition() {
    let clientRect = this.el.getBoundingClientRect()
    if (!clientRect.height || !clientRect.width) {
      return
    }
    return {left: clientRect.left, top: clientRect.top}
  }

  initializeState(event?: Event) {
    let state = this.state
    let initialRectPositionFromBinding = this.binding && this.binding.value && this.binding.value.initialPosition
    let initialRectPositionFromState = state.initialPosition
    let startingDragPosition = this.getRectPosition()
    let initialPosition = initialRectPositionFromBinding || initialRectPositionFromState || startingDragPosition
    this.state = Object.assign(this.state, {
      initialPosition: initialPosition,
      startDragPosition: initialPosition,
      currentDragPosition: initialPosition,
      initialMousePos: this.getInitialMousePosition(event)
    })
    this.updateElementStyle()
  }

  handlePositionChanged(event?: Event, changePositionType?: number) {
    let state = this.state
    let posDiff = {x: 0, y: 0}
    if (state.currentDragPosition && state.startDragPosition) {
      posDiff.x = state.currentDragPosition.left - state.startDragPosition.left
      posDiff.y = state.currentDragPosition.top - state.startDragPosition.top
    }
    let currentPosition = state.currentDragPosition && Object.assign({}, state.currentDragPosition)
    if (changePositionType === ChangePositionType.End) {
      this.binding.value && this.binding.value.onDragEnd && state && this.binding.value.onDragEnd(posDiff, currentPosition, event)
    } else if (changePositionType === ChangePositionType.Start) {
      this.binding.value && this.binding.value.onDragStart && state && this.binding.value.onDragStart(posDiff, currentPosition, event)
    } else {
      this.binding.value && this.binding.value.onPositionChange && state && this.binding.value.onPositionChange(posDiff, currentPosition, event)
    }
  }
}
